/* 
 * [New BSD License (BSD 3-Clause License)]
 * Copyright (c) 2012, Max Bechtold
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 *     - Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     - Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     - Neither the name of the Rel2Xml Project nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package maxbe.rel2xml.grammar;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import maxbe.rel2xml.generator.Column;
import maxbe.rel2xml.generator.dml.Entity;
import maxbe.rel2xml.generator.dml.ResultColumn;
import maxbe.rel2xml.util.Config;

import org.apache.log4j.Logger;


/**
 * Helper methods to build DML statements from SQL strings
 *  
 * @author Max Bechtold
 *
 */
public class QueryBuilder {
	private static Logger log = Logger.getLogger(QueryBuilder.class);

	Map<String, Entity> currentEntities;
	
	Map<String, ResultColumn> resultColumns;
	
	boolean useAliases;
	
	SchemaBuilder sb;
	
	public QueryBuilder(SchemaBuilder sb) {
		this.sb = sb;
		currentEntities = new LinkedHashMap<String, Entity>();
		resultColumns = new HashMap<String, ResultColumn>();
		useAliases = false;
	}

	void reset() {
		currentEntities.clear();
		resultColumns.clear();
		useAliases = false;
	}
	
	/**
	 * Resolves one or (with wildcards) multiple columns using the passed 
	 * (maybe aliased) column reference.
	 */
	List<ResultColumn> resolveColumns(String columnName) {
		List<ResultColumn> columns = new ArrayList<ResultColumn>();
		ResultColumn column = null;
	
		if ("*".equals(columnName)) {
			for (Entity entity : currentEntities.values()) {
				column = new ResultColumn("*", entity);
				columns.add(column);
			}
		} else if (columnName.indexOf('.') != -1) {
			if (!useAliases) {
				String msg = String
						.format("You cannot qualify column '%s' with an alias as there are none specified in the from clause.",
								columnName);
				log.error(msg);
				throw (new TranslateDmlException(msg));
			}
	
			// Lookup columns using tables aliases
			String[] split = columnName.split("\\.");
			Entity entity = currentEntities.get(split[0]);
			if (entity == null) {
				String msg = String.format(
						"No table with alias '%s' specified.", split[0]);
				log.error(msg);
				throw (new TranslateDmlException(msg));
			}
	
			String key = entity.getAlias() + '.' + split[1];
			column = resultColumns.get(key);
			if (column == null) {
				column = new ResultColumn(
						sb.getReferredColumn(entity.getTable(), split[1]), entity);
				resultColumns.put(key, column);
			}
	
			columns.add(column);
		} else {
			if (useAliases) {
				String msg = String.format("Missing alias for column '%s'.",
						columnName);
				log.error(msg);
				throw (new TranslateDmlException(msg));
			}
	
			// Search through list of joined tables to find columns
			// Take first matching column, no ambiguity testing (user's
			// responsibility)
	
			for (Entity entity : currentEntities.values()) {
				Column col = entity.getTable().getColumn(columnName);
				if (col != null) {
					String key = entity.getAlias() + '.' + columnName;
					column = resultColumns.get(key);
					if (column == null) {
						column = new ResultColumn(col, entity);
						resultColumns.put(key, column);
					}
					break;
				}
			}
			if (column == null) {
				String msg = String.format(
						"No table in from clause has a column named '%s'.",
						columnName);
				log.error(msg);
				throw (new TranslateDmlException(msg));
			}
	
			columns.add(column);
		}
	
		return columns;
	}

	ResultColumn resolveColumn(String columnName) {
		return resolveColumns(columnName).get(0);
	}

	void addEntity(String alias, Entity entity) {
		if (useAliases == (alias == null)) {
			String msg = "Cannot have tables with and without aliases at the same time.";
			log.error(msg);
			throw (new TranslateDmlException(msg));
		}
	
		if (alias != null && currentEntities.containsKey(alias)) {
			String msg = String.format("Alias '%s' is already in use.", alias);
			log.error(msg);
			throw (new TranslateDmlException(msg));
		}
	
		if (alias != null && alias.equals(Config.ROOT_NODE_VAR)) {
			String msg = String.format("Alias '" + Config.ROOT_NODE_VAR
					+ "' is reserved, use another one.", alias);
			log.error(msg);
			throw (new TranslateDmlException(msg));
		}
	
		if (alias == null) {
			if (currentEntities.containsValue(entity)) {
				String msg = String
						.format("Cannot refer multiple times to base tables without using aliases.");
				log.error(msg);
				throw (new TranslateDmlException(msg));
			} else {
				String name = entity.getTable().getName();
				boolean found = false;
				for (int i = 1; i < name.length() && !found; i++) {
					alias = name.substring(0, i).toLowerCase();
					if (!currentEntities.containsKey(alias)
							&& !alias.equals("doc")) {
						found = true;
					}
				}
				if (!found) {
					alias += System.currentTimeMillis();
				}
	
				entity.setAlias(alias);
			}
		}
	
		currentEntities.put(alias, entity);
	}

}
